package tv.dyndns.kishibe.server;

import java.awt.Canvas;
import java.awt.Image;
import java.awt.image.AreaAveragingScaleFilter;
import java.awt.image.BufferedImage;
import java.awt.image.FilteredImageSource;
import java.awt.image.ImageFilter;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.imageio.ImageIO;

import org.apache.commons.codec.digest.DigestUtils;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.util.EntityUtils;

import tv.dyndns.kishibe.client.packet.PacketImageLink;
import tv.dyndns.kishibe.client.packet.PacketProblem;
import tv.dyndns.kishibe.server.database.Database;
import tv.dyndns.kishibe.server.database.DatabaseException;
import tv.dyndns.kishibe.server.database.ProblemProcessable;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.io.ByteStreams;
import com.google.common.io.Closeables;
import com.google.common.io.Files;
import com.google.inject.Inject;

public class ImageManager {
	private static Logger logger = Logger.getLogger(ImageManager.class.getName());
	private static final String CACHE_ROOT_PATH = "/tmp/qmaclone/image";
	private static final String CACHE_INPUT_PATH = CACHE_ROOT_PATH + "/input";
	private static final String CACHE_OUTPUT_PATH = CACHE_ROOT_PATH + "/output";
	private final Database database;
	private final HttpClient httpClient;
	private final Object downloadLock = new Object();
	private final Object resizeLock = new Object();
	private volatile List<PacketImageLink> erroImageLinks = new ArrayList<PacketImageLink>();
	private final Runnable commandUpdate = new Runnable() {
		@Override
		public void run() {
			try {
				update();
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
	};

	/**
	 * URLに含まれるパラメーターを保持する
	 * 
	 * @author nodchip
	 * 
	 */
	public static class Parameter {
		public final String url;
		public final int width;
		public final int height;

		public Parameter(String url, int width, int height) {
			this.url = url;
			this.width = width;
			this.height = height;
		}

		public String getHashString() {
			return toHashString(url + "+" + width + "+" + height);
		}
	}

	@Inject
	public ImageManager(Database database, HttpClient httpClient, ThreadPool threadPool) {
		this.database = database;
		this.httpClient = httpClient;

		new File(CACHE_ROOT_PATH).mkdirs();
		new File(CACHE_INPUT_PATH).mkdirs();
		new File(CACHE_OUTPUT_PATH).mkdirs();
		ImageIO.setCacheDirectory(new File(CACHE_ROOT_PATH));
		ImageIO.setUseCache(true);

		threadPool.addDailyTask(commandUpdate);
	}

	/**
	 * ダウンロードした画像ファイルのキャッシュ格納先ファイルを返す
	 * 
	 * @param url
	 * @return
	 */
	@VisibleForTesting
	File getInputCacheFile(String url) {
		String hash = toHashString(url);
		return new File(CACHE_INPUT_PATH + "/" + hash.substring(0, 2) + "/" + hash.substring(2));
	}

	/**
	 * リサイズ後の画像ファイルのキャッシュ格納先ファイルを返す
	 * 
	 * @param parameter
	 * @return
	 */
	@VisibleForTesting
	File getOutputCacheFile(Parameter parameter) {
		String hash = parameter.getHashString();
		return new File(CACHE_OUTPUT_PATH + "/" + hash.substring(0, 2) + "/" + hash.substring(2));
	}

	/**
	 * 画像ファイルをダウンロードする
	 * 
	 * @param url
	 *            ダウンロード元url
	 * @param file
	 *            出力先ファイル
	 * @return HTTPステータスコード
	 * @throws IOException
	 */
	@VisibleForTesting
	int downloadToFile(String url, File file) throws IOException {
		HttpEntity httpEntity = null;
		InputStream is = null;
		OutputStream os = null;

		try {
			HttpResponse httpResponse = httpClient.execute(new HttpGet(url));
			httpEntity = httpResponse.getEntity();
			// httpEntityに代入してからreturnしないとconsumeContent()が呼ばれない
			int statusCode = httpResponse.getStatusLine().getStatusCode();
			if (statusCode / 100 != 2) {
				return statusCode;
			}

			is = httpEntity.getContent();
			os = new BufferedOutputStream(new FileOutputStream(file));
			ByteStreams.copy(is, os);
			return statusCode;

		} finally {
			Closeables.closeQuietly(is);
			Closeables.closeQuietly(os);
			if (httpEntity != null) {
				EntityUtils.consume(httpEntity);
			}
		}
	}

	/**
	 * 画像をリサイズする
	 * 
	 * @param inputFile
	 *            リサイズ元画像ファイル
	 * @param width
	 *            リサイズ後の画像の幅
	 * @param height
	 *            　リサイズ後の画像の高さ
	 * @param outputFile
	 *            リサイズ後画像ファイル
	 * @throws IOException
	 */
	@VisibleForTesting
	void resizeImage(File inputFile, int width, int height, File outputFile) throws IOException {
		BufferedImage inputImage = ImageIO.read(inputFile);

		if (inputImage == null) {
			throw new IOException("ダウンロードした画像ファイル形式が判別できませんでした inputFile:" + inputFile);
		}

		ImageFilter imageFilter = new AreaAveragingScaleFilter(width, height);
		Image middleImage = new Canvas().createImage(new FilteredImageSource(
				inputImage.getSource(), imageFilter));
		BufferedImage outputImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
		outputImage.createGraphics().drawImage(middleImage, 0, 0, width, height, null);

		ImageIO.write(outputImage, "jpeg", outputFile);

		logger.log(
				Level.INFO,
				String.format("%d bytes -> %d bytes (%s->%s)", inputFile.length(),
						outputFile.length(), inputFile.getPath(), outputFile.getPath()));
	}

	/**
	 * 文字列のハッシュを返す
	 * 
	 * @param data
	 * @return
	 */
	@VisibleForTesting
	static String toHashString(String data) {
		return DigestUtils.shaHex(data);
	}

	public long getLastModified(Parameter parameter) {
		File outputCacheFile = getOutputCacheFile(parameter);
		return outputCacheFile.lastModified();
	}

	public void writeToStream(Parameter parameter, OutputStream outputStream) throws IOException {
		String url = parameter.url;
		int width = parameter.width;
		int height = parameter.height;
		File inputCacheFile = getInputCacheFile(url);
		Files.createParentDirs(inputCacheFile);
		File outputCacheFile = getOutputCacheFile(parameter);
		Files.createParentDirs(outputCacheFile);

		// 画像がダウンロードされていなければダウンロードする
		// double-check
		// TODO(nodchip): 実行速度が早くシンプルな方法に変更する
		if (!inputCacheFile.isFile()) {
			synchronized (downloadLock) {
				if (!inputCacheFile.isFile()) {
					// BugTrack-QMAClone/434 - QMAClone wiki
					// http://kishibe.dyndns.tv/qmaclone/wiki/wiki.cgi?page=BugTrack%2DQMAClone%2F434#1330156832
					File tempFile = File.createTempFile("ImageManager-download", null);
					int statusCode = downloadToFile(url, tempFile);
					if (statusCode / 100 != 2) {
						throw new IOException("ダウンロードに失敗しました url:" + url + " statusCode:"
								+ statusCode);
					}
					tempFile.renameTo(inputCacheFile);
				}
			}
		}

		// 画像がリサイズされていなければリサイズする
		if (!outputCacheFile.isFile()) {
			synchronized (resizeLock) {
				if (!outputCacheFile.isFile()) {
					// BugTrack-QMAClone/434 - QMAClone wiki
					// http://kishibe.dyndns.tv/qmaclone/wiki/wiki.cgi?page=BugTrack%2DQMAClone%2F434#1330156832
					File tempFile = File.createTempFile("ImageManager-resize", null);
					resizeImage(inputCacheFile, width, height, tempFile);
					tempFile.renameTo(outputCacheFile);
				}
			}
		}

		writeFileToStream(outputCacheFile, outputStream);
	}

	@VisibleForTesting
	void writeFileToStream(File outputCacheFile, OutputStream outputStream) throws IOException {
		try (OutputStream os = outputStream;
				InputStream is = new BufferedInputStream(new FileInputStream(outputCacheFile))) {
			ByteStreams.copy(is, outputStream);
		}
	}

	private boolean isImage(File file) {
		try {
			return ImageIO.read(file) != null;
		} catch (IOException e) {
			return false;
		}
	}

	private synchronized void update() throws DatabaseException {
		final List<PacketImageLink> imageLinks = Lists.newArrayList();
		final Map<String, Integer> urlToStatusCode = Maps.newHashMap();

		database.processProblems(new ProblemProcessable() {
			@Override
			public void process(PacketProblem problem) throws Exception {
				for (String url : problem.getImageUrls()) {
					File inputCacheFile = getInputCacheFile(url);

					if (!urlToStatusCode.containsKey(url)) {
						Files.createParentDirs(inputCacheFile);
						int statusCode = downloadToFile(url, inputCacheFile);
						urlToStatusCode.put(url, statusCode);
					}

					int statusCode = urlToStatusCode.get(url);

					// 正常取得かつ正常画像の場合はエラー出力をしない
					if (statusCode / 100 == 2 && isImage(inputCacheFile)) {
						continue;
					}

					PacketImageLink imageLink = new PacketImageLink();
					imageLink.problemId = problem.id;
					imageLink.url = url;
					imageLink.statusCode = statusCode;
					imageLinks.add(imageLink);
				}
			}
		});

		Collections.sort(imageLinks);

		this.erroImageLinks = imageLinks;
	}

	public List<PacketImageLink> getErrorImageLinks() {
		return erroImageLinks;
	}
}
